iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0
Modern Web

就是個Go,我也可以啦!GOGO系列 第 20

2023鐵人賽Day 20 Go語言解鎖:實踐並發編程的策略與技巧

  • 分享至 

  • xImage
  •  

我們介紹了很多併發及go的內部原理後,我們終於要來碰實作啦,感謝大家的耐心,這集的篇幅會偏長,請容我細細道來

在開始前,我們要來複習一下上一節的觀念

  • goroutine: 在Go語言中,goroutine可以看作是這些進程,它們運行在相同的地址空間,並且可以獨立執行,每個goroutine運行一個函數或方法,並且可以與其他goroutine通信和同步
  • channel: 在Go中,channel用於goroutines之間的數據通信和同步
  • select: Select語句提供了一種方式來處理多個channel的發送和接收,可以監聽多個channel操作,並執行對應的塊

創建模式

Go語言使用 go 關鍵字 + 函數/方法創建goroutine

go fmt.Println("create a goroutine")

但只是這樣還沒辦法發揮go的強大之處,CSP 模型強調通過通信來共享記憶體,而不是通過共享記憶體來通信。在 Go 中,這種通信主要是通過 channel 來實現的。channel 是 Go 中的一個核心特性

package main

import (
	"fmt"
	"time"
)

func worker(msgCh chan string, replyCh chan string) {
	// 從 msgCh 接收消息
	msg := <-msgCh
	fmt.Println("Worker received:", msg)

	// 發送一個回應到 replyCh
	replyCh <- "Work done!"
}

func main() {
	msgCh := make(chan string)
	replyCh := make(chan string)

	// 啟動 worker goroutine
	go worker(msgCh, replyCh)

	// 發送一個消息到 worker
	msgCh <- "Do some work"

	// 接收 worker 的回應
	reply := <-replyCh
	fmt.Println("Main received:", reply)

	// 等待一秒鐘來觀察輸出,否則主函數結束程式就會退出
	time.Sleep(1 * time.Second)
}


其實對一個原本是寫ruby及js的我來說,要理解這個概念真的不太容易,在 JavaScript 和 Ruby(或其他非併發語言)中,當一個函數被調用並返回結果後,該函數的執行上下文通常就結束了,goroutine 是如何保持活動狀態的,即使它的主要功能(例如一個函數或任務)已經完成?

我們先來看運作的關係圖
https://ithelp.ithome.com.tw/upload/images/20231005/20150980ORRlsNDmCF.png

  • 當你使用 go 關鍵字啟動一個 goroutine(例如 go worker(msgCh, replyCh))時,
    • 這個 goroutine 會在背景中並行運行
    • 它不會阻塞主函數的執行
    • 當 worker 函數在一個新的 goroutine 中運行時,主函數仍然會繼續執行下一行代碼
  • channel 在 Go 中用於在 goroutines 之間傳遞數據和同步它們的執行
    • 當一個 goroutine 從一個 channel 讀取數據時(例如 msg := <-msgCh)
      • 它會等待,直到有數據可用
    • 當一個 goroutine 寫入一個 channel(例如 replyCh <- "Work done!")時
      • 它也會等待,直到其他 goroutine 從該 channel 讀取數據

退出模式

當一個 goroutine 的函數執行完畢,這個 goroutine 就會自動結束,不需要手動進行終止或管理,但有一些服務程序可能會對goroutine有著優雅退出的需求,這邊我們提幾個退出模式

分離模式

這個模式通常用於那些一旦啟動就不需要再進行交互或管理的 goroutine。它們通常執行一個特定的任務,然後自行結束

  • 一次性任務

    go func() {
        // 做一些工作...
    }()
    
    
  • 常駐後台的特定任務

    • 這些 goroutine 會一直運行,執行例如監控或其他後台任務的功能
    go func() {
        for {
            // 做一些持續的工作...
        }
    }()
    

join模式

在傳統的執行緒模型中,一個主執行緒(父執行緒)可以等待它創建的子執行緒完成工作並獲取其結果。這通常是通過 pthread_join 函數來實現的

在 Go 語言中,我們也經常需要主函數(或主 goroutine)等待一個或多個由它創建的 goroutine 完成工作

等待一個goroutine退出

使用channel

package main

import (
	"fmt"
)

func worker(done chan bool) {
	fmt.Println("Worker is working...")
	// 模擬工作
	done <- true // 發送一個信號表示工作已經完成
}

func main() {
	done := make(chan bool, 1) // 創建一個通道

	go worker(done) // 啟動一個 goroutine

	<-done // 等待 goroutine 發送完成的信號
	fmt.Println("All goroutines have finished executing")
}

channel 用於同步 goroutine 的執行。當 goroutine 完成工作時,它向 done channel 發送一個信號。主函數通過 <-done 接收這個信號,如果沒有收到信號,主函數會一直阻塞

獲取goroutine的退出狀態

package main

import (
	"fmt"
	"time"
)

func worker(resultCh chan int) {
	fmt.Println("Worker: 開始工作...")
	// 模擬一個長時間的工作
	time.Sleep(2 * time.Second)
	fmt.Println("Worker: 工作完成!")
	// 將結果發送到 channel
	resultCh <- 42
}

func main() {
	// 創建一個用於接收工作結果的 channel
	resultCh := make(chan int)

	fmt.Println("Main: 啟動 worker goroutine...")
	// 啟動 worker goroutine
	go worker(resultCh)

	fmt.Println("Main: 等待 worker 完成...")
	// 從 channel 接收 worker 的結果
	result := <-resultCh
	fmt.Println("Main: worker 完成,結果是:", result)
}

函數在 result := <-resultCh 這行代碼處等待,直到 worker goroutine 將結果發送到 resultCh channel,一旦 worker 發送了結果,主函數接收它並繼續執行

等待多個goroutine退出

當你在 Go 中啟動多個 goroutines 並且你想要在主 goroutine(通常是你的 main 函數)中等待它們全部完成時,你可以使用 sync.WaitGroup。WaitGroup 是一個計數信號量,我們可以使用它來等待一組 goroutines 完成

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // 在函數結束時調用 Done 來通知 WaitGroup 這個 worker 已經完成

	fmt.Printf("Worker %d: 開始工作...\n", id)
	time.Sleep(time.Second) // 模擬一秒的工作時間
	fmt.Printf("Worker %d: 工作完成\n", id)
}

func main() {
	var wg sync.WaitGroup // 創建一個 WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1) // 每啟動一個 goroutine 就向 WaitGroup 增加一個計數
		go worker(i, &wg) // 啟動一個 worker goroutine
	}

	// 等待所有的 worker goroutine 完成
	wg.Wait()
	fmt.Println("所有工作都已完成")
}

  • 我們創建了一個 sync.WaitGroup 變量 wg。
  • 在每次啟動一個新的 worker goroutine 之前,我們使用 wg.Add(1) 增加 WaitGroup 的計數。
  • worker 函數在開始時使用 defer wg.Done() 來確保在函數結束時 WaitGroup 的計數將減少。
  • wg.Wait() 會阻塞,直到 WaitGroup 的計數變為 0,即所有 worker goroutines 都已調用 Done(),表示它們已完成。

管道模式

管道(Pipeline)模式是一種將一系列處理步驟連接在一起的模式,每個步驟都是一個處理單元,數據在這些處理單元之間通過 channel(通道)傳遞。每個處理單元都是一個 goroutine,所以數據處理步驟是並行執行的

package main

import (
	"fmt"
	"sync"
)

func producer(ch chan int) {
	for i := 0; i < 10; i++ {
		ch <- i // 生產數據
	}
	close(ch) // 關閉 channel
}

func processor(inputCh chan int, outputCh chan int) {
	for num := range inputCh { // 處理數據
		outputCh <- num * num // 平方
	}
	close(outputCh) // 關閉 channel
}

func consumer(ch chan int, wg *sync.WaitGroup) {
	for num := range ch { // 消費數據
		fmt.Println(num)
	}
	wg.Done() // 通知 WaitGroup 任務完成
}

func main() {
	numCh := make(chan int)
	squareCh := make(chan int)
	var wg sync.WaitGroup

	go producer(numCh) // 啟動生產者
	go processor(numCh, squareCh) // 啟動處理者

	wg.Add(1)
	go consumer(squareCh, &wg) // 啟動消費者

	wg.Wait() // 等待所有 goroutine 完成
}

  • producer 函數生成 0 到 9 的數字並將它們發送到 numCh channel
  • processor 函數從 numCh channel 讀取數字,將它們平方,然後將結果發送到 squareCh channel
  • consumer 函數從 squareCh channel 讀取數字並打印它們

https://ithelp.ithome.com.tw/upload/images/20231005/20150980ZusiTxOWTB.png

以上就是goroutine/channel/併發 的基本用法,用法還有非常多種,在熟悉基本的用法後我們可以再更進一步,直到這一篇為止,我們已經說了非常多基本概念了,明天開始我們要來進入WEB,正式進入後端領域啦~


上一篇
2023鐵人賽Day 19 面向併發,Go 語言借鏡 CSP 模型的一場大師級的併發設計之旅
下一篇
2023鐵人賽Day 21 Go X Http
系列文
就是個Go,我也可以啦!GOGO30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言